Alors que la société française se caractérise par une grande défiance de la population vis-à-vis de ses élus et responsables politiques, nous nous pencherons dans ce projet sur la question de l'assiduité des députés au cours de leur mandat. Nous verrons notamment grâce à différents outils (boîtes de Tukey, matrice de nuages de points, analyse en composantes principales, clustering, régression linéaire...) si l'on peut dresser un portrait type pour caractériser le député assidu ou absentéiste.
%matplotlib inline
import csv
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import mpl_toolkits.mplot3d as plt3d
import seaborn as sns
On récupère sur le site citoyen nosdeputes.fr une base de données synthétisant l'activité parlementaire sur les 12 derniers mois. Pour chaque député, cette table, que nous avons convertie au format .tsv, contient des informations relatives à son état civil, à la circonscription dont il est le représentant, à son éventuel parti politique et surtout à son travail parlementaire (semaines de présence à l'Assemblée nationale, rédaction de rapports, participation à des commissions...).
# Conversion du fichier tsv en DataFrame.
df = pd.read_csv("../Données/nosdeputes.fr_synthese_2020-10-24.tsv", sep='\t')
df
| id | nom | nom_de_famille | prenom | sexe | date_naissance | lieu_naissance | num_deptmt | nom_circo | num_circo | ... | hemicycle_interventions | hemicycle_interventions_courtes | amendements_proposes | amendements_signes | amendements_adoptes | rapports | propositions_ecrites | propositions_signees | questions_ecrites | questions_orales | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 32 | Damien Abad | Abad | Damien | H | 1980-04-05 | Nîmes (Gard) | 01 | Ain | 5 | ... | 253 | 307 | 19 | 1736 | 63 | 0 | 0 | 25 | 38 | 9 |
| 1 | 43 | Caroline Abadie | Abadie | Caroline | F | 1976-09-07 | Saint-Martin-d'Hères (Isère) | 38 | Isère | 8 | ... | 17 | 8 | 14 | 362 | 196 | 0 | 0 | 3 | 3 | 0 |
| 2 | 493 | Jean-Félix Acquaviva | Acquaviva | Jean-Félix | H | 1973-03-19 | Bastia (Haute-Corse) | 2B | Haute-Corse | 2 | ... | 109 | 9 | 186 | 2309 | 89 | 0 | 0 | 4 | 22 | 3 |
| 3 | 152 | Lénaïck Adam | Adam | Lénaïck | H | 1992-02-19 | Saint Laurent du Maroni (Guyane) | 973 | Guyane | 2 | ... | 3 | 0 | 15 | 581 | 175 | 0 | 0 | 4 | 1 | 0 |
| 4 | 234 | Damien Adam | Adam | Damien | H | 1989-06-28 | Orléans (Loiret) | 76 | Seine-Maritime | 1 | ... | 24 | 7 | 74 | 687 | 225 | 0 | 0 | 4 | 10 | 2 |
| ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... |
| 534 | 26 | Martine Wonner | Wonner | Martine | F | 1964-03-27 | Hayange (Moselle) | 67 | Bas-Rhin | 4 | ... | 61 | 6 | 249 | 1581 | 157 | 0 | 0 | 3 | 14 | 3 |
| 535 | 215 | Hubert Wulfranc | Wulfranc | Hubert | H | 1956-12-17 | Rouen (Seine-Maritime) | 76 | Seine-Maritime | 3 | ... | 211 | 143 | 895 | 2776 | 70 | 0 | 0 | 8 | 35 | 16 |
| 536 | 130 | Hélène Zannier | Zannier | Hélène | F | 1972-09-19 | Saint-Avold (Moselle) | 57 | Moselle | 7 | ... | 9 | 3 | 25 | 475 | 214 | 0 | 0 | 4 | 12 | 0 |
| 537 | 31 | Jean-Marc Zulesi | Zulesi | Jean-Marc | H | 1988-06-06 | Marseille (Bouches-du-Rhône) | 13 | Bouches-du-Rhône | 8 | ... | 40 | 37 | 105 | 1111 | 279 | 0 | 0 | 4 | 52 | 4 |
| 538 | 329 | Michel Zumkeller | Zumkeller | Michel | H | 1966-01-21 | Belfort (Territoire de Belfort) | 90 | Territoire de Belfort | 2 | ... | 39 | 11 | 18 | 1603 | 61 | 0 | 0 | 3 | 15 | 4 |
539 rows × 40 columns
Ce jeu de données est plutôt complet, mais il lui manque une variable qui pourrait nous intéresser pour notre étude : le statut du député (sortant, élu pour la première fois, ancien député ou arrivé en cours de mandat).
Nous allons donc compléter la table avec la variable "statut" du tableau disponible sur la page http://www2.assemblee-nationale.fr/elections/liste/2017/resultats/RESULTAT.
# On récupère le code source de la page afin d'en extraire le tableau.
from urllib import request
import bs4
request_text = request.urlopen("http://www2.assemblee-nationale.fr/elections/liste/2017/resultats/RESULTAT").read()
page = bs4.BeautifulSoup(request_text, "html")
tableau_html = page.find("table") # On extrait le tableau d'intérêt de la page HTML.
Nous avons récupéré le code HTML du tableau, récupérons maintenant ses entêtes.
entetes = tableau_html.find('thead')
entetes = entetes.find('tr')
entetes = entetes.find_all('th')
entetes = [entete.text.strip() for entete in entetes]
print(entetes)
['Civ.', 'Nom', 'Prénom', 'Département', 'Circ.', 'Statut', 'Tour', 'Nuance']
Puis complétons un dictionnaire avec ses lignes et transformons le en DataFrame.
dict_tableau = {}
for entete in entetes:
dict_tableau[entete] = []
corps_tableau = tableau_html.find('tbody')
lignes_tableau = corps_tableau.find_all('tr')
for ligne in lignes_tableau:
colonnes = ligne.find_all('td')
for i, element in enumerate(colonnes):
dict_tableau[entetes[i]].append(element.text.strip())
df1 = pd.DataFrame.from_dict(dict_tableau)
On affiche les premières lignes pour s'assurer qu'on obtient bien le tableau souhaité.
df1.head()
| Civ. | Nom | Prénom | Département | Circ. | Statut | Tour | Nuance | |
|---|---|---|---|---|---|---|---|---|
| 0 | M. | ABAD | DAMIEN | AIN | 5 | SORTANT | 2 | LR |
| 1 | Mme | ABADIE | CAROLINE | ISERE | 8 | ELUE POUR LA 1ERE FOIS | 2 | REM |
| 2 | Mme | ABBA | BÉRANGÈRE | HAUTE-MARNE | 1 | ELUE POUR LA 1ERE FOIS | 2 | REM |
| 3 | M. | ACQUAVIVA | JEAN-FÉLIX | HAUTE-CORSE | 2 | ELU POUR LA 1ERE FOIS | 2 | REG |
| 4 | M. | ADAM | LÉNAÏCK | GUYANE | 2 | ELU POUR LA 1ERE FOIS | 2 | REM |
Maintenant que nous disposons de nos deux bases, nous allons travailler sur celles-ci de sorte à les rendre plus maniables : nous procédons donc au nettoyage des données.
Dans le tableau webscrapé, conservons uniquement les variables "Nom", "Prénom" et "Statut" et convertissons la casse des modalités et des variables, dans l'optique de les comparer avec celles de la table principale.
df1 = df1[['Nom','Prénom','Statut']] # On ne conserve que 3 variables.
# On modifie la casse des modalités.
df1["Nom"] = df1["Nom"].str.lower()
df1["Prénom"] = df1["Prénom"].str.lower()
df1["Statut"] = df1["Statut"].str.lower()
On concatène le nom et le prénom pour ne conserver que le patronyme complet et le comparer avec celui de l'autre base.
df1['Nom'] = df1['Prénom'] + ' ' + df1['Nom']
df1 = df1.drop(['Prénom'], axis=1) # On retire donc la variable 'Prénom', désormais inutile.
Afin d'uniformiser les styles d'écriture, on enlève désormais les mots vides (ici les déterminants "de" fréquemment présents dans les noms de famille), car ils ne sont pas écrits de la même manière dans les deux bases. On "déféminise" également les modalités de la variable 'Statut' (eg remplacer 'sortante' par 'sortant'), les informations sur le sexe étant déjà enregistrées dans une variable dédiée.
replace_values_ean = {' de ':' ', ' (de) ':' '}
replace_values_fem = {'elue pour la 1ere fois' : 'elu pour la 1ere fois', 'sortante' : 'sortant', 'ancienne' : 'ancien'}
# On crée la fonction de nettoyage qui retire le déterminant "de".
def clean_dataset(data):
data.replace({'nom': replace_values_ean, 'Nom' : replace_values_ean}, regex=True, inplace=True)
data.replace({'Statut': replace_values_fem}, inplace=True)
return data
# On crée une copie de la table principale pour ne pas modifier la base initiale.
df_new = df.copy()
df_new['nom'] = df_new['nom'].str.lower()
clean_dataset(df_new) # On procède au nettoyage sur la base initiale.
| id | nom | nom_de_famille | prenom | sexe | date_naissance | lieu_naissance | num_deptmt | nom_circo | num_circo | ... | hemicycle_interventions | hemicycle_interventions_courtes | amendements_proposes | amendements_signes | amendements_adoptes | rapports | propositions_ecrites | propositions_signees | questions_ecrites | questions_orales | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 32 | damien abad | Abad | Damien | H | 1980-04-05 | Nîmes (Gard) | 01 | Ain | 5 | ... | 253 | 307 | 19 | 1736 | 63 | 0 | 0 | 25 | 38 | 9 |
| 1 | 43 | caroline abadie | Abadie | Caroline | F | 1976-09-07 | Saint-Martin-d'Hères (Isère) | 38 | Isère | 8 | ... | 17 | 8 | 14 | 362 | 196 | 0 | 0 | 3 | 3 | 0 |
| 2 | 493 | jean-félix acquaviva | Acquaviva | Jean-Félix | H | 1973-03-19 | Bastia (Haute-Corse) | 2B | Haute-Corse | 2 | ... | 109 | 9 | 186 | 2309 | 89 | 0 | 0 | 4 | 22 | 3 |
| 3 | 152 | lénaïck adam | Adam | Lénaïck | H | 1992-02-19 | Saint Laurent du Maroni (Guyane) | 973 | Guyane | 2 | ... | 3 | 0 | 15 | 581 | 175 | 0 | 0 | 4 | 1 | 0 |
| 4 | 234 | damien adam | Adam | Damien | H | 1989-06-28 | Orléans (Loiret) | 76 | Seine-Maritime | 1 | ... | 24 | 7 | 74 | 687 | 225 | 0 | 0 | 4 | 10 | 2 |
| ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... |
| 534 | 26 | martine wonner | Wonner | Martine | F | 1964-03-27 | Hayange (Moselle) | 67 | Bas-Rhin | 4 | ... | 61 | 6 | 249 | 1581 | 157 | 0 | 0 | 3 | 14 | 3 |
| 535 | 215 | hubert wulfranc | Wulfranc | Hubert | H | 1956-12-17 | Rouen (Seine-Maritime) | 76 | Seine-Maritime | 3 | ... | 211 | 143 | 895 | 2776 | 70 | 0 | 0 | 8 | 35 | 16 |
| 536 | 130 | hélène zannier | Zannier | Hélène | F | 1972-09-19 | Saint-Avold (Moselle) | 57 | Moselle | 7 | ... | 9 | 3 | 25 | 475 | 214 | 0 | 0 | 4 | 12 | 0 |
| 537 | 31 | jean-marc zulesi | Zulesi | Jean-Marc | H | 1988-06-06 | Marseille (Bouches-du-Rhône) | 13 | Bouches-du-Rhône | 8 | ... | 40 | 37 | 105 | 1111 | 279 | 0 | 0 | 4 | 52 | 4 |
| 538 | 329 | michel zumkeller | Zumkeller | Michel | H | 1966-01-21 | Belfort (Territoire de Belfort) | 90 | Territoire de Belfort | 2 | ... | 39 | 11 | 18 | 1603 | 61 | 0 | 0 | 3 | 15 | 4 |
539 rows × 40 columns
clean_dataset(df1) # On procède au nettoyage sur la table webscrapée.
| Nom | Statut | |
|---|---|---|
| 0 | damien abad | sortant |
| 1 | caroline abadie | elu pour la 1ere fois |
| 2 | bérangère abba | elu pour la 1ere fois |
| 3 | jean-félix acquaviva | elu pour la 1ere fois |
| 4 | lénaïck adam | elu pour la 1ere fois |
| ... | ... | ... |
| 572 | martine wonner | elu pour la 1ere fois |
| 573 | hubert wulfranc | elu pour la 1ere fois |
| 574 | hélène zannier | elu pour la 1ere fois |
| 575 | jean-marc zulesi | elu pour la 1ere fois |
| 576 | michel zumkeller | sortant |
577 rows × 2 columns
On élabore maintenant une fonction qui retire les accents.
import unicodedata, string
import unidecode
def remove_accent(s) :
return ''.join((c for c in unicodedata.normalize('NFD', s) if unicodedata.category(c) != 'Mn'))
# Retrait des accents sur tous les noms complets des députés dans les deux bases.
df_new["nom"]=df_new["nom"].map(lambda x: remove_accent(x))
df1["Nom"]=df1["Nom"].map(lambda x: remove_accent(x))
On peut désormais joindre les deux tables pour ajouter à la table initiale la variable portant sur le statut du député.
df_work = pd.merge(df_new, df1, how='left', left_on='nom', right_on='Nom')
df_work = df_work.drop('Nom', axis=1) # On retire la variable "Nom" qui fait désormais doublon.
On va remplacer les valeurs manquantes pour la variable "Statut" par la modalité "arrivé en cours de mandat". En effet, cela concerne des députés présents dans la base mis à jour mais non présents dans la base webscrapée qui correspond aux députés élus en 2017 ; ils sont donc arrivés au cours des trois dernières années.
for i in range(len(df_work)) :
if df_work['Statut'].isnull()[i] == True :
df_work['Statut'][i] = 'arrive en cours de mandat'
On crée désormais une variable âge à partir de la variable date_naissance. Cela va nous permettre de répartir les députés en tranches d'âge de façon à analyser l'influence de l'âge sur l'assiduité.
import datetime
df_work['age'] = 0 # On initialise à 0 la valeur de la variable age pour chaque individu.
adj = datetime.date.today()
for i in range(len(df_work)) :
date = datetime.datetime.strptime(df_work["date_naissance"][i], '%Y-%m-%d')
df_work['age'][i] = adj.year - date.year - ((adj.month, adj.day) < (date.month, date.day))
# On calcule la différence entre l'année actuelle et l'année de naissance de chaque député, en corrigeant d'une unité si l'anniversaire n'est pas encore arrivé.
df_work = df_work.sort_values("age") # On réordonne la base dans l'ordre croissant de l'âge des députés.
# On répartit maintenant les députés dans différentes tranches d'âge.
df_work['tranche_age'] = 0
for i in range(len(df_work)) :
if 20 <= df_work['age'][i] < 30 :
df_work['tranche_age'][i] = '20-30 ans'
if 30 <= df_work['age'][i] < 40 :
df_work['tranche_age'][i] = '30-40 ans'
if 40 <= df_work['age'][i] < 50 :
df_work['tranche_age'][i] = '40-50 ans'
if 50 <= df_work['age'][i] < 60 :
df_work['tranche_age'][i] = '50-60 ans'
if 60 <= df_work['age'][i] < 70 :
df_work['tranche_age'][i] = '60-70 ans'
if df_work['age'][i] >= 70 :
df_work['tranche_age'][i] = '+ de 70 ans'
On poursuit le nettoyage en remplaçant la modalité "0" de la variable profession par la modalité "Aucune", pour des questions de lisibilité.
for i in range(len(df_work)) :
if df_work['profession'][i] == '0' :
df_work['profession'][i] = 'Aucune'
Il peut être intéressant pour notre étude d'avoir une variable plus agrégée que le département indiquant la zone géographique d'origine des députés : la région administrative. A partir d'une table trouvée sur internet (et copiée dans le répertoire 'Données'), nous allons construire cette nouvelle variable.
df_regions = pd.read_csv("../Données/departments_regions_france_2016.csv")
dep2reg = {} # Dictionnaire assignant le nom de la région à un numéro de département.
for i in range(len(df_regions)):
dep2reg[df_regions["departmentCode"][i]] = df_regions["regionName"][i]
df_work["region"] = ""
for i in range(len(df_work)):
try:
df_work["region"][i] = dep2reg[df_work["num_deptmt"][i]]
except KeyError:
df_work["region"][i] = "Outre mer" # Si le numéro n'est pas dans la base, c'est qu'il s'agit d'un département d'Outre-Mer.
On peut maintenant analyser les variables présentes dans la base définitive df_work afin de sélectionner celles que nous pouvons éliminer dans le cadre de notre étude.
df_work.columns # On affiche la liste des variables.
Index(['id', 'nom', 'nom_de_famille', 'prenom', 'sexe', 'date_naissance',
'lieu_naissance', 'num_deptmt', 'nom_circo', 'num_circo',
'mandat_debut', 'mandat_fin', 'ancien_depute', 'groupe_sigle',
'parti_ratt_financier', 'sites_web', 'emails', 'anciens_mandats',
'profession', 'place_en_hemicycle', 'url_an', 'id_an', 'slug',
'url_nosdeputes', 'url_nosdeputes_api', 'nb_mandats', 'twitter',
'semaines_presence', 'commission_presences', 'commission_interventions',
'hemicycle_interventions', 'hemicycle_interventions_courtes',
'amendements_proposes', 'amendements_signes', 'amendements_adoptes',
'rapports', 'propositions_ecrites', 'propositions_signees',
'questions_ecrites', 'questions_orales', 'Statut', 'age', 'tranche_age',
'region'],
dtype='object')# On retire les variables que l'on juge inutiles pour notre étude.
df_work = df_work.drop(['id', 'nom_de_famille', 'prenom', 'date_naissance', 'lieu_naissance','mandat_debut', 'mandat_fin', 'ancien_depute','parti_ratt_financier', 'sites_web','emails', 'anciens_mandats', 'place_en_hemicycle', 'url_an','id_an', 'slug', 'url_nosdeputes', 'url_nosdeputes_api', 'twitter'], axis=1)
df_work.head() # On regarde les premières lignes de la base de travail définitive.
| nom | sexe | num_deptmt | nom_circo | num_circo | groupe_sigle | profession | nb_mandats | semaines_presence | commission_presences | ... | amendements_adoptes | rapports | propositions_ecrites | propositions_signees | questions_ecrites | questions_orales | Statut | age | tranche_age | region | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 139 | typhanie degois | F | 73 | Savoie | 1 | LREM | Juriste | 1 | 21 | 23 | ... | 207 | 0 | 0 | 3 | 36 | 1 | elu pour la 1ere fois | 27 | 20-30 ans | Auvergne-Rhône-Alpes |
| 383 | ludovic pajot | H | 62 | Pas-de-Calais | 10 | NI | Aucune | 1 | 23 | 15 | ... | 1 | 0 | 0 | 2 | 54 | 5 | elu pour la 1ere fois | 27 | 20-30 ans | Hauts-de-France |
| 3 | lenaick adam | H | 973 | Guyane | 2 | LREM | Cadre supérieur (entreprises privée) | 1 | 8 | 5 | ... | 175 | 0 | 0 | 4 | 1 | 0 | elu pour la 1ere fois | 28 | 20-30 ans | Outre mer |
| 235 | pierre henriet | H | 85 | Vendée | 5 | LREM | Professeur du secondaire et technique | 1 | 31 | 26 | ... | 188 | 0 | 0 | 3 | 9 | 0 | elu pour la 1ere fois | 29 | 20-30 ans | Pays de la Loire |
| 298 | sandrine le feur | F | 29 | Finistère | 4 | LREM | Agriculteur-propriétaire exploitant | 1 | 21 | 39 | ... | 217 | 0 | 0 | 3 | 9 | 1 | elu pour la 1ere fois | 29 | 20-30 ans | Bretagne |
5 rows × 25 columns
On procède à une analyse descriptive des données afin d'identifier les premières grandes tendances qui guideront notre travail. Les boîtes à moustaches (ou boîtes de Tukey, dites boxplots en anglais) permettent par exemple d'étudier les statistiques liées aux semaines de présence des députés à l'Assemblée nationale, en distinguant selon le sexe, le parti politique ou encore l'âge. On utilise ici la librairie seaborn.
fig, ax = plt.subplots(1, 2, figsize=(10,4))
sns.set_style("whitegrid")
sns.histplot(data=df_work, x="semaines_presence", kde=True, ax=ax[0])
ax[0].set_xlabel('Semaines de présence')
ax[0].set_ylabel('Nombre de députés')
ax[0].set_title('Semaines de présence des députés')
sns.boxplot(y="semaines_presence",data=df_work, ax=ax[1])
ax[1].set_xlabel("Boîte de Tukey des semaines de présence")
ax[1].set_ylabel('')
plt.show()
fig, ax = plt.subplots(1, 2, figsize=(10,4))
sns.set_style("whitegrid")
sns.histplot(data=df_work, x="semaines_presence", hue="sexe", kde=True, ax=ax[0])
ax[0].set_xlabel('Semaines de présence')
ax[0].set_ylabel('Nombre de députés')
ax[0].set_title('Semaines de présence des députés selon le sexe')
sns.boxplot(x="sexe", y="semaines_presence", data=df_work, ax=ax[1])
ax[1].set_xlabel("Boîte de Tukey des semaines de présence selon le sexe")
ax[1].set_ylabel('')
plt.show()
fig, ax = plt.subplots(2, 1, figsize=(10,8))
sns.set_style("whitegrid")
sns.kdeplot(data=df_work, x="semaines_presence", hue="groupe_sigle", ax=ax[0])
ax[0].set_title('Distributions des semaines de présence des députés selon leur parti')
ax[0].set_xlabel('Semaines de présence')
ax[0].set_ylabel('Proportion des députés')
sns.boxplot(x="groupe_sigle", y="semaines_presence", data=df_work, ax=ax[1])
ax[1].set_xlabel("Boîte de Tukey des semaines de présence selon le parti")
ax[1].set_ylabel("")
plt.show()
fig, ax = plt.subplots(2, 1, figsize=(10,8))
sns.set_style("whitegrid")
sns.kdeplot(data=df_work, x="semaines_presence", hue="tranche_age", ax=ax[0])
ax[0].set_title("Distributions des semaines de présence des députés selon leur tranche d'âge")
ax[0].set_xlabel('Semaines de présence')
ax[0].set_ylabel('Proportion des députés')
sns.boxplot(x="tranche_age", y="semaines_presence", data=df_work, ax=ax[1])
ax[1].set_xlabel("Boîte de Tukey des semaines de présence selon la tranche d'âge")
ax[1].set_ylabel("")
plt.show()
L'observation de ces boîtes à moustaches donne à voir quelques premières tendances :
Voyons maintenant si la circonscription du député a une quelconque influence sur son assiduité. Pour ce faire, nous allons représenter le taux d'assiduité de chaque député en termes de semaines de présence à l'Assemblée nationale sur la carte des circonscriptions législatives. Nous importons donc les librairies geopandas et geoviews ainsi que les autres packages et extensions nécessaires à la réalisation de notre carte.
import geoviews as gv
import geopandas as gpd
import geoviews.feature as gf
import xarray as xr
from cartopy import crs
gv.extension('bokeh')
sf = gpd.read_file('../Données/france-circonscriptions-legislatives-2012.json')
sf.head()
| ID | code_dpt | nom_dpt | nom_reg | num_circ | code_reg | geometry | |
|---|---|---|---|---|---|---|---|
| 0 | 33004 | 33 | GIRONDE | AQUITAINE-LIMOUSIN-POITOU-CHARENTES | 4 | 75 | POLYGON ((-0.45495 44.95342, -0.40932 44.94761... |
| 1 | 38001 | 38 | ISERE | AUVERGNE-RHONE-ALPES | 1 | 84 | POLYGON ((5.80529 45.20620, 5.75468 45.19679, ... |
| 2 | 59010 | 59 | NORD | NORD-PAS-DE-CALAIS-PICARDIE | 10 | 32 | POLYGON ((3.05875 50.78071, 3.08067 50.77286, ... |
| 3 | 33007 | 33 | GIRONDE | AQUITAINE-LIMOUSIN-POITOU-CHARENTES | 7 | 75 | POLYGON ((-0.61065 44.82247, -0.60305 44.81848... |
| 4 | ZA001 | ZA | GUADELOUPE | GUADELOUPE | 1 | 01 | MULTIPOLYGON (((-61.49348 16.35364, -61.45097 ... |
On procède à quelques nettoyages supplémentaires sur les noms de département dans les deux tables afin d'uniformiser les styles d'écriture (pas d'accent, minuscules).
df2 = df_work.copy()
# On travaille sur une copie de la table de travail pour ne pas la modifier.
df2["nom_circo"] = df2["nom_circo"].map(lambda x: remove_accent(x))
df2["nom_circo"] = df2["nom_circo"].str.lower()
sf["nom_dpt"] = sf["nom_dpt"].str.lower()
On exclut les départements d'outre-mer de notre étude, car il apparaît évident que leurs semaines de présence sont très faibles en raison de l'éloignement géographique. On s'intéresse donc exclusivement à la métropole ici.
sf = sf.loc[-sf['code_dpt'].isin(['ZA','ZB','ZC','ZD',
'ZM','ZN','ZS','ZX','ZW','ZP'])]
# On ne garde que les lignes correspondant aux circonscriptions métropolitaines.
Avant de pouvoir joindre nos deux tables, nous concaténons le numéro de circonscription et le département associé dans les deux bases de façon à obtenir une variable d'identification commune.
sf["id_circo"] = sf['num_circ'] + ' ' + sf['nom_dpt']
df2["id_circo"]=0
for i in range(len(df2)) :
df2["id_circo"][i] = str(df2['num_circo'][i]) + ' ' + df2['nom_circo'][i]
# On réalise la jointure sur la nouvelle variable id_circo.
jf = sf.merge(df2, how='left', left_on='id_circo', right_on='id_circo')
On peut désormais représenter les semaines de présence des députés en fonction de leur circonscription. Les circonscriptions grisées sur la carte ci-dessous correspondent à celles pour lesquelles nous n'avons pas de données sur l'activité du député.
from geoviews import dim
circo = gv.Polygons(jf, vdims=['id_circo', 'semaines_presence'])
circo.opts(width=600, height=600, toolbar='above', color=dim('semaines_presence'),
colorbar=True, tools=['hover'], aspect='equal')
La lecture de cette carte ne permet pas de dégager de lien particulier entre l'origine géographique et la présence à l'Assemblée. Il existe des députés ruraux très présents et d'autres très peu assidus, il en va de même pour les députés urbains.
De manière générale, l'analyse descriptive de nos données reste très insuffisante pour répondre à la problématique initiale, bien que les boxplots aient permis de dégager certaines tendances.
Nous avons jusqu'à présent étudié uniquement le nombre de semaines de présence des députés sur les bancs de l'Assemblée, sans nous préoccuper des autres indicateurs d'assiduité. Nous allons nous demander dans quelle mesure la présence est suffisante pour révéler l'assiduité, notamment en étudiant les corrélations entre les différents indicateurs.
Les indicateurs d'assiduité que nous avons à disposition sont 'semaines_presence', 'commission_presences', 'commission_interventions', 'hemicycle_interventions','hemicycle_interventions_courtes', 'amendements_proposes', 'amendements_signes', 'amendements_adoptes', 'rapports', 'propositions_ecrites', 'propositions_signees','questions_ecrites' et 'questions_orales'.
# On trace la matrice des nuages de points afin de percevoir les premiers liens entre les variables d'assiduité.
sns.set(font_scale=0.3)
sns.pairplot(df_work[['semaines_presence', 'commission_presences', 'commission_interventions', 'hemicycle_interventions','hemicycle_interventions_courtes', 'amendements_proposes', 'amendements_signes', 'amendements_adoptes', 'rapports', 'propositions_ecrites', 'propositions_signees','questions_ecrites', 'questions_orales']], height=0.6, markers=".", aspect=1.1)
plt.show()
On constate de nombreuses corrélations entre les variables, cependant la variable donnant les semaines de présence semble par exemple assez décorrélée avec le nombre de questions écrites. Il faut donc prendre en compte un nombre plus élevé de variables pour dégager correctement l'assiduité des députés.
Nous allons commencer par regrouper les députés en groupes relativement homogènes (clusters) selon l'ensemble des variables d'assiduité.
Afin d'éviter que des variables l'emportent sur d'autres par des effets d'échelle, nous allons normaliser (centrer et réduire) les variables sur lesquelles nous obtiendrons les clusters.
# On crée une nouvelle table avec le numéro du député en index et les variables d'assiduité.
df_clusters = df_work[['semaines_presence', 'commission_presences', 'commission_interventions', 'hemicycle_interventions','hemicycle_interventions_courtes', 'amendements_proposes', 'amendements_signes', 'amendements_adoptes', 'rapports', 'propositions_ecrites', 'propositions_signees','questions_ecrites', 'questions_orales']]
import sklearn # On importe scikit-learn.
# On centre et réduit les variables de manière à effectuer une ACP normée.
from sklearn.preprocessing import StandardScaler
sc = StandardScaler()
Z = sc.fit_transform(df_clusters)
Z
array([[-7.86259121e-01, -7.67223361e-01, -4.76110854e-01, ...,
-5.05906629e-01, 1.23583513e+00, -6.12158245e-01],
[-4.99136041e-01, -1.17818822e+00, -4.25038063e-01, ...,
-6.82286754e-01, 2.36772151e+00, 6.48408303e-01],
[-2.65255914e+00, -1.69189430e+00, -5.61232172e-01, ...,
-3.29526504e-01, -9.65055053e-01, -9.27299882e-01],
...,
[-4.99136041e-01, -4.07629108e-01, 5.68528285e-04, ...,
7.28754245e-01, 1.42448286e+00, 3.33266666e-01],
[ 3.62233199e-01, 1.57447577e-01, 1.36762638e-01, ...,
2.13979524e+00, -7.13524747e-01, 1.27869158e+00],
[-2.93968222e+00, -1.69189430e+00, -5.78256436e-01, ...,
2.13979524e+00, 9.84304822e-01, -9.27299882e-01]])On vérifie que les moyennes sont nulles et les écarts-types unitaires ; c'est bien le cas : la normalisation a bien été effectuée.
print(np.mean(Z, axis=0))
print(np.std(Z, axis=0, ddof=0))
[-4.28434859e-17 -6.59130553e-17 2.30695693e-17 -4.61391387e-17 3.29565276e-18 6.59130553e-18 -4.61391387e-17 1.05460888e-16 -6.09695761e-17 5.60260970e-17 9.22782774e-17 2.63652221e-17 -6.59130553e-18] [1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1.]
Afin de déterminer des groupes de députés en fonction de leur assiduité, on va désormais procéder à un clustering, en s'appuyant sur la méthode des k-moyennes (k-means), très répandue en partitionnement des données. Cette étape va nous permettre de constituer des groupes de députés d'assiduité comparable ; nous dégagerons ensuite les caractéristiques partagées par les députés appartenant au même cluster.
from sklearn.cluster import KMeans
from sklearn.metrics import silhouette_score # On importe la métrique "silhouette" qui nous servira à déterminer le nombre de clusters optimal.
# On applique la méthode des k-means sur les variables d'assiduité centrées et réduites, en supposant pour l'instant par défaut qu'il existe 3 clusters.
kmeans = KMeans(n_clusters=3, random_state=146355805)
# random_state est la graine aléatoire specifiée pour rendre les résultats reproductibles.
kmeans.fit(Z)
# On trie les index en fonction des différents groupes de députés (ie des clusters).
idk = np.argsort(kmeans.labels_)
# Affichage des observations et de leurs groupes.
print(pd.DataFrame({"député" : df_clusters.index[idk], "cluster" : kmeans.labels_[idk]}))
député cluster 0 139 0 1 205 0 2 114 0 3 238 0 4 504 0 .. ... ... 534 230 2 535 463 2 536 532 2 537 187 2 538 462 2 [539 rows x 2 columns]
On s'intéresse désormais à la répartition des députés au sein des différents clusters.
nb_clusters = kmeans.labels_.tolist()
print(nb_clusters.count(0)) # Comptage du nombre de députés dans le cluster 1.
print(nb_clusters.count(1)) # Comptage du nombre de députés dans le cluster 2.
print(nb_clusters.count(2)) # Comptage du nombre de députés dans le cluster 3.
426 105 8
On observe que l'un des clusters, restreint à 8 observations, semble contenir des députés très marginalisés, dont nous analyserons plus tard les caractéristiques. Vérifions tout d'abord le nombre optimal de clusters en nous appuyant sur la métrique "silhouette" : celle-ci évalue grâce à un coefficient (compris entre -1 et 1) propre à chaque point la différence entre la distance moyenne de ce point avec les points du même cluster et la distance moyenne de ce même point avec les points des autres clusters. Si le point est bien placé, il doit donc être plus proche en moyenne des points de son cluster ; ceci se matérialise par un coefficient de silhouette proche de 1. A l'inverse, un coefficient proche de -1 témoigne d'un mauvais partitionnement. On va donc tracer le score moyen de silhouette en fonction du nombre de clusters, et retenir le nombre de clusters correspondant au score maximal.
# On va faire varier le nombre de clusters dans un intervalle réaliste, disons de 2 à 10.
res = np.arange(9, dtype="double")
for k in np.arange(9):
km = KMeans(n_clusters=k+2)
km.fit(Z)
res[k] = silhouette_score(Z, km.labels_)
print(res)
[0.39121398 0.38631287 0.15581256 0.16821042 0.18181887 0.19409984 0.20620682 0.21130385 0.20132463]
plt.figure(figsize=(8,4))
plt.plot(np.arange(2,11,1), res)
plt.title("Coefficient de silhouette moyen en fonction du nombre de clusters", size=16)
plt.xlabel("Nombre de clusters", size=10)
plt.ylabel("Score de silhouette moyen", size=10)
plt.show()
A la vue de ce graphique, le choix optimal semble se porter sur 2 clusters (3 clusters ne semble pas être un mauvais choix non plus). Interprétons ceux-ci au moyen d'une analyse en composantes principales (ACP).
Les données étant centrées et réduites, nous pouvons débuter l'ACP.
from sklearn.decomposition import PCA
# Instanciation de l'objet PCA
acp = PCA(svd_solver='full')
On remarque que le nombre de composantes n'est pas spécifié, il correspond donc par défaut au nombre de variables, à savoir 13 (ce qu'on vérifie dans la cellule suivante). Nous stockons maintenant les coordonnées factorielles dans la variable coord grâce à la fonction fit_transform().
# Calcul des coordonnées factorielles
coord = acp.fit_transform(Z)
# Vérification du nombre de composantes principales
print(acp.n_components_)
13
Désormais, nous allons afficher le pourcentage de la variance expliquée par chaque axe factoriel.
print(acp.explained_variance_ratio_)
[0.30292818 0.16774724 0.10467894 0.07784485 0.07354728 0.06130974 0.0578616 0.04991936 0.03846265 0.02819897 0.01826355 0.01285006 0.00638759]
On remarque que le premier axe explique environ 30% de l'information disponible, et les trois premiers près de 57 % : les autres axes ne semblent à première vue pas complètement anecdotiques. Nous allons donc tracer l'éboulis des valeurs propres (qui correspondent aussi à la variance expliquée par chaque axe) afin d'appliquer la méthode dite du coude.
plt.figure(figsize=(8,4))
plt.bar(np.arange(1,acp.n_components_+1),acp.explained_variance_ratio_*100)
plt.title("Part de la variance expliquée par chaque axe factoriel", size=16, fontweight='bold')
plt.ylabel("Variance expliquée par l'axe factoriel (en %)", size=10)
plt.xlabel("Axe factoriel", size=10)
plt.show()
Au vu du diagramme ci-dessus, nous allons choisir de garder les trois premières composantes factorielles (même si 4 composantes correspondraient mieux d'après la méthode du coude, nous choisissons 3 composantes pour des questions de représentation). Nous les représentons dans la figure en trois dimensions ci-dessous, avec une couleur différente pour chaque cluster trouvé à la section précédente.
fig = plt.figure(figsize=(12,8))
ax = fig.add_subplot(111, projection='3d')
ax.set_xlim(-5,8)
ax.set_ylim(-5,8)
ax.set_zlim(-5,8)
for i in range(len(coord[:,0])):
if kmeans.labels_[i] == 0:
ax.text(coord[i,0], coord[i,1], coord[i,2], str(df_clusters.index[i]), size=6, color='r')
elif kmeans.labels_[i] == 1:
ax.text(coord[i,0], coord[i,1], coord[i,2], str(df_clusters.index[i]), size=6, color='g')
else:
ax.text(coord[i,0], coord[i,1], coord[i,2], str(df_clusters.index[i]), size=6, color='b')
ax.scatter(coord[:,0], coord[:,1], coord[:,2])
plt.show()
Nous pouvons constater que le nuage de points se compose de deux groupes : l'un quasiment aplati dans le plan (Oxy), qui est le premier plan factoriel, l'autre ayant des cotes elevées. Le cluster des 8 députés se distingue largement en constituant le second groupe ; les deux autres clusters semblent regroupés dans le plan (Oxy).
On constate néanmoins que les députés du petit cluster écrasent l'information donnée par les autres députés. Afin d'analyser plus finement les différents types de députés, nous allons retirer ces députés de notre étude après avoir interprété leur type.
Ces derniers députés se distinguant des autres par une cote élevée, interprétons les différents axes de l'ACP. On calcule pour cela les corrélations des variables avec chaque axe factoriel.
n = len(df_clusters)
p = len(df_clusters.columns)
eigval = (n-1)/n*acp.explained_variance_
sqrt_eigval = np.sqrt(eigval)
corvar = np.zeros((p,p))
for k in range(p):
corvar[:,k] = acp.components_[k,:] * sqrt_eigval[k]
print(pd.DataFrame({'Variable':df_clusters.columns,'Cor_axe_1':corvar[:,0],'Cor_axe_2':corvar[:,1], 'Cor_axe_3':corvar[:,2]}))
Variable Cor_axe_1 Cor_axe_2 Cor_axe_3 0 semaines_presence 0.557067 0.600929 -0.191771 1 commission_presences 0.425776 0.662578 -0.302692 2 commission_interventions 0.496364 0.325125 -0.420058 3 hemicycle_interventions 0.648652 0.488061 0.485997 4 hemicycle_interventions_courtes 0.399350 0.453602 0.745994 5 amendements_proposes 0.695054 -0.235665 -0.117199 6 amendements_signes 0.763213 -0.375204 -0.062249 7 amendements_adoptes -0.428367 0.497853 -0.223568 8 rapports 0.027278 0.174714 -0.383634 9 propositions_ecrites 0.495685 -0.237651 0.004301 10 propositions_signees 0.587427 -0.415955 0.040091 11 questions_ecrites 0.497084 -0.334478 0.013451 12 questions_orales 0.729486 -0.121882 -0.216111
Puis on représente ces corrélations sur la sphère unité de $\mathbb{R}^3$.
r = 1
pi = np.pi
cos = np.cos
sin = np.sin
phi, theta = np.mgrid[0.0:pi:100j, 0.0:2.0*pi:100j]
x = r*sin(phi)*cos(theta)
y = r*sin(phi)*sin(theta)
z = r*cos(phi)
fig = plt.figure(figsize=(12,12))
ax = fig.add_subplot(111, projection='3d')
ax.plot_surface(x, y, z, rstride=1, cstride=1, color='c', alpha=0.1, linewidth=0)
ax.add_line(plt3d.art3d.Line3D([-1,1], [0,0], [0,0]))
ax.add_artist(plt3d.art3d.Text3D(x=1, y=0, z=0, text='Axe 1', zdir='x', size=8))
ax.add_line(plt3d.art3d.Line3D([0,0], [-1,1], [0,0]))
ax.add_artist(plt3d.art3d.Text3D(x=0, y=1, z=0, text='Axe 2', zdir='y', size=8))
ax.add_line(plt3d.art3d.Line3D([0,0], [0,0], [-1,1]))
ax.add_artist(plt3d.art3d.Text3D(x=0, y=0, z=1, text='Axe 3', zdir='z', size=8))
ax.quiver(np.zeros(p), np.zeros(p), np.zeros(p), corvar[:,0], corvar[:,1], corvar[:,2], color='r')
for i in range(p):
ax.add_artist(plt3d.art3d.Text3D(x=corvar[i,0], y=corvar[i,1], z=corvar[i,2], text=df_clusters.columns[i], zdir=(corvar[i,0], corvar[i,1], corvar[i,2]), size=6))
ax.set_xlim([-1,1])
ax.set_ylim([-1,1])
ax.set_zlim([-1,1])
plt.show()
On constate que le troisième axe différencie essentiellement les députés effectuant de nombreuses interventions (courtes ou non) dans l'hémicycle. Le groupe de députés se situant dans le groupe du haut sur le nuage de points est donc celui des députés les plus actifs en séance.
Retirons les et recommençons le clustering et l'ACP.
df_clusters2 = df_clusters.drop(df_clusters.index[kmeans.labels_ == 2])
Z2 = sc.fit_transform(df_clusters2)
res = np.arange(9, dtype="double")
for k in np.arange(9):
km = KMeans(n_clusters=k+2)
km.fit(Z2)
res[k] = silhouette_score(Z2, km.labels_)
plt.figure(figsize=(8,4))
plt.plot(np.arange(2,11,1), res)
plt.title("Coefficient de silhouette moyen en fonction du nombre de clusters", size=16)
plt.xlabel("Nombre de clusters", size=10)
plt.ylabel("Score de silhouette moyen", size=10)
plt.show()
Cette fois-ci, le nombre de clusters optimal est clairement 2.
kmeans2 = KMeans(n_clusters=2, random_state=3535864)
kmeans2.fit(Z2)
idk = np.argsort(kmeans2.labels_)
print(pd.DataFrame({"député" : df_clusters2.index[idk], "cluster" : kmeans2.labels_[idk]}))
nb_clusters2 = kmeans2.labels_.tolist()
print(nb_clusters2.count(0)) # Comptage du nombre de députés dans le cluster 1.
print(nb_clusters2.count(1)) # Comptage du nombre de députés dans le cluster 2.
député cluster 0 139 0 1 137 0 2 526 0 3 205 0 4 114 0 .. ... ... 526 288 1 527 256 1 528 412 1 529 533 1 530 283 1 [531 rows x 2 columns] 437 94
Nous avons là encore un cluster plus petit que l'autre, mais d'une taille nettement plus significative.
acp2 = PCA(svd_solver='full')
coord2 = acp2.fit_transform(Z2)
plt.figure(figsize=(8,4))
plt.bar(np.arange(1, acp2.n_components_+1), acp2.explained_variance_ratio_*100)
plt.title("Part de la variance expliquée par chaque axe factoriel", size=16, fontweight='bold')
plt.ylabel("Variance expliquée par l'axe factoriel (en %)", size=10)
plt.xlabel("Axe factoriel", size=10)
plt.show()
Les deux premiers axes expliquent maintenant 50% de la variance à eux seuls.
Représentons les observations par cluster dans le premier plan factoriel.
fig, axes = plt.subplots(figsize=(10,6))
axes.set_xlim(-5,10)
axes.set_ylim(-5,10)
for i in range(len(coord2[:,0])):
if kmeans2.labels_[i] == 0:
axes.text(coord2[i,0], coord2[i,1], str(df_clusters2.index[i]), size=6, color='r')
else :
axes.text(coord2[i,0], coord2[i,1], str(df_clusters2.index[i]), size=6, color='g')
axes.scatter(coord2[:,0], coord2[:,1])
plt.show()
On remarque que les députés du cluster de gauche (en rouge) se distinguent essentiellement des députés du cluster de droite (en vert) par leur abscisse, c'est-à-dire par leur coordonnée sur le premier axe factoriel. Nous allons donc maintenant calculer la corrélation des variables avec les deux premiers axes factoriels puis tracer le cercle des corrélations afin d'interpréter ces deux axes.
n = len(df_clusters2)
p = len(df_clusters2.columns)
eigval = (n-1)/n*acp2.explained_variance_
sqrt_eigval = np.sqrt(eigval)
corvar = np.zeros((p,p))
for k in range(p):
corvar[:,k] = acp2.components_[k,:] * sqrt_eigval[k]
print(pd.DataFrame({'Variable':df_clusters2.columns,'Cor_axe_1':corvar[:,0],'Cor_axe_2':corvar[:,1]}))
Variable Cor_axe_1 Cor_axe_2 0 semaines_presence 0.552689 0.605856 1 commission_presences 0.413833 0.690726 2 commission_interventions 0.567610 0.424653 3 hemicycle_interventions 0.817380 0.262870 4 hemicycle_interventions_courtes 0.684536 0.172765 5 amendements_proposes 0.715332 -0.193385 6 amendements_signes 0.760858 -0.365307 7 amendements_adoptes -0.415093 0.544406 8 rapports 0.054197 0.260701 9 propositions_ecrites 0.460106 -0.260975 10 propositions_signees 0.550433 -0.453888 11 questions_ecrites 0.473171 -0.351876 12 questions_orales 0.769974 -0.039379
fig, axes = plt.subplots(figsize=(8,8))
axes.set_xlim(-1.05,1.05)
axes.set_ylim(-1.05,1.05)
for j in range(p):
axes.arrow(0, 0, corvar[j,0], corvar[j,1], head_width=0.005, head_length=0.01, fc='grey', ec='grey')
plt.annotate(df_clusters2.columns[j], (corvar[j,0],corvar[j,1]), size=8, color='r')
plt.plot([-1,1],[0,0],color='silver',linestyle='-',linewidth=1)
plt.plot([0,0],[-1,1],color='silver',linestyle='-',linewidth=1)
cercle = plt.Circle((0,0), 1, color='blue', fill=False)
axes.add_artist(cercle)
plt.show()
L'immense majorité des variables d'assiduité semblent très fortement corrélées avec le premier axe factoriel (à l'exception des rapports et des amendements adoptés), alors que le deuxième axe factoriel ne semble pas vraiment rendre compte de l'assiduité des députés. On va donc s'intéresser en particulier aux députés présentant une coordonnée élevée selon le premier axe factoriel.
On va commencer par étudier les caractéristiques des députés de chacun des deux clusters afin de dégager des tendances claires quant aux facteurs pouvant expliquer l'assiduité supérieure des députés du cluster vert par rapport à celle des députés du cluster rouge. Pour ce faire, on scinde notre table de travail en deux sous-tables contenant chacune les députés de l'un des deux clusters.
A = pd.DataFrame({"député" : df_clusters2.index[idk], "cluster" : kmeans2.labels_[idk]})
R = [] # Liste qui contiendra les index des députés du cluster rouge.
V = [] # Liste qui contiendra les index des députés du cluster vert.
for i in range(len(A)) :
if A['cluster'][i] == 0 :
R.append(A['député'][i])
else :
V.append(A['député'][i])
# On vérifie qu'il y a bien 437 éléments dans la liste R et 94 dans la liste R.
len(R), len(V)
(437, 94)
df_cluster_rouge = df_work.loc[R] # Sous-table avec les députés du cluster rouge.
df_cluster_vert = df_work.loc[V] # Sous-table avec les députés du cluster vert.
On va maintenant comparer les statistiques obtenues sur les variables dans les deux cas. Commençons par les variables quantitatives.
df_cluster_rouge.describe()
| num_circo | nb_mandats | semaines_presence | commission_presences | commission_interventions | hemicycle_interventions | hemicycle_interventions_courtes | amendements_proposes | amendements_signes | amendements_adoptes | rapports | propositions_ecrites | propositions_signees | questions_ecrites | questions_orales | age | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| count | 437.000000 | 437.000000 | 437.000000 | 437.000000 | 437.000000 | 437.000000 | 437.000000 | 437.000000 | 437.000000 | 437.000000 | 437.000000 | 437.000000 | 437.000000 | 437.000000 | 437.000000 | 437.000000 |
| mean | 4.590389 | 1.260870 | 25.066362 | 34.926773 | 22.485126 | 39.993135 | 25.681922 | 57.013730 | 920.439359 | 162.464531 | 0.109840 | 0.105263 | 4.363844 | 13.473684 | 1.945080 | 51.908467 |
| std | 3.593342 | 0.460008 | 6.477745 | 17.963371 | 40.759058 | 55.352146 | 59.534640 | 95.201307 | 792.468492 | 75.542210 | 0.373207 | 0.392469 | 3.405725 | 14.442304 | 1.881027 | 11.196160 |
| min | 1.000000 | 1.000000 | 0.000000 | 0.000000 | 0.000000 | 0.000000 | 0.000000 | 0.000000 | 10.000000 | 0.000000 | 0.000000 | 0.000000 | 0.000000 | 0.000000 | 0.000000 | 27.000000 |
| 25% | 2.000000 | 1.000000 | 21.000000 | 22.000000 | 4.000000 | 8.000000 | 2.000000 | 12.000000 | 482.000000 | 137.000000 | 0.000000 | 0.000000 | 3.000000 | 2.000000 | 1.000000 | 44.000000 |
| 50% | 3.000000 | 1.000000 | 26.000000 | 32.000000 | 12.000000 | 20.000000 | 6.000000 | 29.000000 | 646.000000 | 174.000000 | 0.000000 | 0.000000 | 4.000000 | 9.000000 | 2.000000 | 52.000000 |
| 75% | 6.000000 | 2.000000 | 29.000000 | 46.000000 | 23.000000 | 47.000000 | 20.000000 | 68.000000 | 989.000000 | 211.000000 | 0.000000 | 0.000000 | 5.000000 | 19.000000 | 3.000000 | 60.000000 |
| max | 21.000000 | 3.000000 | 42.000000 | 102.000000 | 376.000000 | 403.000000 | 546.000000 | 873.000000 | 4683.000000 | 503.000000 | 3.000000 | 3.000000 | 32.000000 | 64.000000 | 12.000000 | 82.000000 |
df_cluster_vert.describe()
| num_circo | nb_mandats | semaines_presence | commission_presences | commission_interventions | hemicycle_interventions | hemicycle_interventions_courtes | amendements_proposes | amendements_signes | amendements_adoptes | rapports | propositions_ecrites | propositions_signees | questions_ecrites | questions_orales | age | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| count | 94.000000 | 94.000000 | 94.000000 | 94.000000 | 94.000000 | 94.000000 | 94.000000 | 94.000000 | 94.000000 | 94.000000 | 94.000000 | 94.000000 | 94.000000 | 94.000000 | 94.000000 | 94.000000 |
| mean | 4.563830 | 1.340426 | 31.882979 | 49.553191 | 86.255319 | 230.787234 | 219.819149 | 850.936170 | 3552.723404 | 90.127660 | 0.106383 | 0.808511 | 12.765957 | 29.734043 | 7.670213 | 52.851064 |
| std | 3.800279 | 0.539876 | 5.407793 | 20.528824 | 90.582575 | 165.031432 | 243.734298 | 957.109389 | 1567.971934 | 49.373981 | 0.309980 | 1.175709 | 8.345272 | 16.024964 | 3.759971 | 10.828038 |
| min | 1.000000 | 1.000000 | 16.000000 | 9.000000 | 3.000000 | 8.000000 | 5.000000 | 11.000000 | 514.000000 | 19.000000 | 0.000000 | 0.000000 | 1.000000 | 0.000000 | 0.000000 | 29.000000 |
| 25% | 2.000000 | 1.000000 | 28.250000 | 34.000000 | 31.250000 | 114.250000 | 54.250000 | 156.500000 | 2517.500000 | 44.250000 | 0.000000 | 0.000000 | 7.000000 | 15.000000 | 5.000000 | 46.250000 |
| 50% | 4.000000 | 1.000000 | 32.000000 | 45.000000 | 56.000000 | 175.500000 | 153.500000 | 416.500000 | 3465.500000 | 83.000000 | 0.000000 | 0.000000 | 11.000000 | 31.500000 | 7.000000 | 54.000000 |
| 75% | 6.000000 | 2.000000 | 35.000000 | 62.250000 | 96.000000 | 320.000000 | 293.750000 | 1027.000000 | 5262.500000 | 120.750000 | 0.000000 | 1.000000 | 16.750000 | 41.750000 | 9.750000 | 61.000000 |
| max | 20.000000 | 3.000000 | 43.000000 | 111.000000 | 500.000000 | 708.000000 | 1531.000000 | 2887.000000 | 7434.000000 | 213.000000 | 1.000000 | 6.000000 | 37.000000 | 53.000000 | 18.000000 | 78.000000 |
Nous constatons que les députés du cluster vert ont bel et bien une assiduité très supérieure à leurs homologues du cluster rouge : les valeurs moyennes et médianes pour les variables de présence ou de participation sont bien plus élevées dans leur cas. La segmentation précédemment effectuée est donc pertinente. En revanche, l'âge ne semble pas avoir d'influence sur cette assiduité : les deux distributions semblent se superposer. Enfin, contre toute attente, cumuler des mandats n'est pas un facteur d'absentéisme ou d'inactivité ; au contraire, les députés les plus assidus ont en moyenne davantage de mandats que les autres, même si cette différence est très légère.
Intéressons-nous désormais aux variables qualitatives, à commencer par le sexe des députés.
display(pd.DataFrame({'Nb dans cluster rouge' : df_cluster_rouge['sexe'].value_counts(), 'Prop dans cluster rouge' : df_cluster_rouge['sexe'].value_counts(normalize=True)})) # Répartition des femmes et des hommes dans le cluster rouge.
display(pd.DataFrame({'Nb dans cluster vert' : df_cluster_vert['sexe'].value_counts(), 'Prop dans cluster vert' : df_cluster_vert['sexe'].value_counts(normalize=True)})) # Répartition des femmes et des hommes dans le cluster vert.
| Nb dans cluster rouge | Prop dans cluster rouge | |
|---|---|---|
| H | 255 | 0.583524 |
| F | 182 | 0.416476 |
| Nb dans cluster vert | Prop dans cluster vert | |
|---|---|---|
| H | 63 | 0.670213 |
| F | 31 | 0.329787 |
On remarque que les femmes sont moins représentées en pourcentage dans le cluster vert, autrement dit celui des députés assidus. Cette tendance apparaissait déjà lors de l'analyse descriptive à laquelle nous avons procédé précédemment.
Voyons désormais si les différents partis politiques sont représentés de la même manière dans les deux clusters.
display(pd.DataFrame({'Nb dans cluster rouge' : df_cluster_rouge['groupe_sigle'].value_counts(), 'Prop dans cluster rouge' : df_cluster_rouge['groupe_sigle'].value_counts(normalize=True)}))
display(pd.DataFrame({'Nb dans cluster vert' : df_cluster_vert['groupe_sigle'].value_counts(), 'Prop dans cluster vert' : df_cluster_vert['groupe_sigle'].value_counts(normalize=True)}))
| Nb dans cluster rouge | Prop dans cluster rouge | |
|---|---|---|
| LREM | 255 | 0.583524 |
| LR | 54 | 0.123570 |
| MODEM | 51 | 0.116705 |
| NI | 23 | 0.052632 |
| AE | 17 | 0.038902 |
| SOC | 12 | 0.027460 |
| LT | 11 | 0.025172 |
| UDI | 11 | 0.025172 |
| GDR | 3 | 0.006865 |
| Nb dans cluster vert | Prop dans cluster vert | |
|---|---|---|
| LR | 36 | 0.382979 |
| LFI | 17 | 0.180851 |
| SOC | 12 | 0.127660 |
| GDR | 12 | 0.127660 |
| LT | 7 | 0.074468 |
| UDI | 5 | 0.053191 |
| AE | 2 | 0.021277 |
| MODEM | 2 | 0.021277 |
| NI | 1 | 0.010638 |
On remarque ici que les députés de la majorité LREM-MODEM sont intégralement dans le cluster des députés les moins assidus, à l'exception de deux députés du MODEM. A l'inverse, les députés de gauche, en particulier du GDR (Groupe de la Gauche démocrate et républicaine) et du groupe LFI (La France insoumise), se distinguent par une présence quasi-exclusive au sein du groupe de députés assidus (ici le cluster vert).
Si l'âge ne semble pas corrélé avec l'assiduité, il apparaît que le sexe et l'appartenance politique le sont davantage : les hommes et les parlementaires de gauche sont globalement plus assidus que les femmes et les parlementaires de la majorité.
Une dernière variable qualitative, le statut des députés, est susceptible d'expliquer l'assiduité. On peut en effet penser a priori que les habitués et les nouveaux élus ne fréquentent pas les bancs de l'hémicycle de la même manière, ni à la même régularité.
display(pd.DataFrame({'Nb dans cluster rouge' : df_cluster_rouge['Statut'].value_counts(), 'Prop dans cluster rouge' : df_cluster_rouge['Statut'].value_counts(normalize=True)}))
display(pd.DataFrame({'Nb dans cluster vert' : df_cluster_vert['Statut'].value_counts(), 'Prop dans cluster vert' : df_cluster_vert['Statut'].value_counts(normalize=True)}))
| Nb dans cluster rouge | Prop dans cluster rouge | |
|---|---|---|
| elu pour la 1ere fois | 331 | 0.757437 |
| sortant | 72 | 0.164760 |
| arrive en cours de mandat | 24 | 0.054920 |
| ancien | 10 | 0.022883 |
| Nb dans cluster vert | Prop dans cluster vert | |
|---|---|---|
| elu pour la 1ere fois | 53 | 0.563830 |
| sortant | 38 | 0.404255 |
| ancien | 2 | 0.021277 |
| arrive en cours de mandat | 1 | 0.010638 |
On constate en effet que les nouveaux élus et les sortants sont inégalement répartis dans nos deux clusters d'assiduité : les députés sortants sont près d'un tiers dans le cluster des assidus, tandis que les nouveaux élus sont pour immense majorité dans le cluster des moins assidus. Les députés arrivés en cours de mandat sont quant à eux le plus souvent peu assidus.
Jusqu'à présent, notre segmentation des députés en deux groupes d'assiduité différente, puis notre analyse descriptive sur ces groupes, nous ont permis de dégager les grandes tendances décrivant le député assidu. Il est en général un homme, de gauche, qui le plus souvent occupait déjà sa fonction au mandat précédent.
On pourrait intuitivement inférer la cause de cette tendance au moyen d'éléments sociologiques généraux :
Les explications ci-dessus ne sont cependant en aucun cas déduites de nos données : nous n'avons identifié que des corrélations et jamais d'effet causal. Afin d'essayer d'expliquer l'assiduité des députés grâce aux variables que nous avons à disposition, nous allons mettre en place un modèle linéaire.
Nous choisissons comme variable explicative la coordonnée des députés sur le premier axe factoriel : cette variable synthétise une grande part de l'information disponible sur l'assiduité et constitue donc un bon "score d'assiduité".
Nous allons effectuer nos régressions linéaires sur les données des députés des deux clusters principaux uniquement (sans les 8 députés très actifs en séance).
# On prépare le DataFrame pour les régressions linéaires.
df_reg = df_work.drop(df_work.index[kmeans.labels_ == 2])
df_reg["assiduite"] = coord2[:,0] # On ajoute notre score d'assiduité.
df_reg = df_reg.drop(['semaines_presence', 'commission_presences', 'commission_interventions', 'hemicycle_interventions','hemicycle_interventions_courtes', 'amendements_proposes', 'amendements_signes', 'amendements_adoptes', 'rapports', 'propositions_ecrites', 'propositions_signees','questions_ecrites', 'questions_orales', 'tranche_age'], axis=1)
# On supprime les variables d'assiduité qui ne nous servent plus.
df_reg.head()
| nom | sexe | num_deptmt | nom_circo | num_circo | groupe_sigle | profession | nb_mandats | Statut | age | region | assiduite | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 139 | typhanie degois | F | 73 | Savoie | 1 | LREM | Juriste | 1 | elu pour la 1ere fois | 27 | Auvergne-Rhône-Alpes | -1.375591 |
| 383 | ludovic pajot | H | 62 | Pas-de-Calais | 10 | NI | Aucune | 1 | elu pour la 1ere fois | 27 | Hauts-de-France | -0.207073 |
| 3 | lenaick adam | H | 973 | Guyane | 2 | LREM | Cadre supérieur (entreprises privée) | 1 | elu pour la 1ere fois | 28 | Outre mer | -2.665537 |
| 235 | pierre henriet | H | 85 | Vendée | 5 | LREM | Professeur du secondaire et technique | 1 | elu pour la 1ere fois | 29 | Pays de la Loire | -1.480353 |
| 298 | sandrine le feur | F | 29 | Finistère | 4 | LREM | Agriculteur-propriétaire exploitant | 1 | elu pour la 1ere fois | 29 | Bretagne | -1.574167 |
Commençons par étudier l'effet du sexe sur l'assiduité. Afin de minimiser le biais de variable omise, nous allons ajouter les variables de contrôle 'age' et 'region' qui sont susceptibles d'avoir un effet à la fois sur le sexe et l'assiduité. Nous ne contrôlons pas par 'groupe_sigle' car cette variable est potentiellement fonction du sexe ; ceci afin de limiter le biais de variable incluse. Concernant la variable 'profession', nous ne l'utiliserons pas dans la suite car les professions des députés sont d'une part trop diversifiées et spécifiques, et il est d'autre part impossible de savoir si le député exerce encore cette profession en parallèle de son mandat.
import statsmodels.formula.api as sm
# On va effectuer la régression linéaire du score d'assiduité sur le sexe, l'âge et la région d'origine des députés.
model = sm.ols('assiduite ~ sexe + age + region', data = df_reg)
result = model.fit()
print(result.summary())
OLS Regression Results
==============================================================================
Dep. Variable: assiduite R-squared: 0.074
Model: OLS Adj. R-squared: 0.047
Method: Least Squares F-statistic: 2.749
Date: Mon, 07 Dec 2020 Prob (F-statistic): 0.000429
Time: 02:56:41 Log-Likelihood: -1134.3
No. Observations: 531 AIC: 2301.
Df Residuals: 515 BIC: 2369.
Df Model: 15
Covariance Type: nonrobust
========================================================================================================
coef std err t P>|t| [0.025 0.975]
--------------------------------------------------------------------------------------------------------
Intercept -0.3844 0.501 -0.767 0.443 -1.368 0.600
sexe[T.H] 0.4071 0.188 2.169 0.031 0.038 0.776
region[T.Bourgogne-Franche-Comté] -0.7051 0.488 -1.446 0.149 -1.663 0.253
region[T.Bretagne] -0.9992 0.509 -1.961 0.050 -2.000 0.002
region[T.Centre-Val de Loire] -0.3755 0.509 -0.738 0.461 -1.376 0.625
region[T.Corse] -0.4277 1.078 -0.397 0.692 -2.546 1.691
region[T.Grand Est] 0.6225 0.410 1.518 0.130 -0.183 1.428
region[T.Hauts-de-France] 0.2716 0.412 0.659 0.510 -0.538 1.081
region[T.Ile-de-France] 0.0149 0.345 0.043 0.965 -0.663 0.693
region[T.Normandie] 0.3128 0.493 0.634 0.526 -0.656 1.282
region[T.Nouvelle-Aquitaine] -0.8116 0.406 -1.997 0.046 -1.610 -0.013
region[T.Occitanie] -0.4855 0.405 -1.198 0.231 -1.282 0.311
region[T.Outre mer] -1.2564 0.437 -2.878 0.004 -2.114 -0.399
region[T.Pays de la Loire] -0.9986 0.480 -2.082 0.038 -1.941 -0.056
region[T.Provence-Alpes-Côte d'Azur] -0.1888 0.436 -0.433 0.665 -1.045 0.668
age 0.0078 0.008 0.953 0.341 -0.008 0.024
==============================================================================
Omnibus: 169.513 Durbin-Watson: 1.952
Prob(Omnibus): 0.000 Jarque-Bera (JB): 417.257
Skew: 1.640 Prob(JB): 2.48e-91
Kurtosis: 5.846 Cond. No. 689.
==============================================================================
Notes:
[1] Standard Errors assume that the covariance matrix of the errors is correctly specified.
Toutes choses égales par ailleurs dans cette régression, le fait d'être un homme augmente en moyenne le score d'assiduité de 0.4 points. Le fait d'être un homme aurait donc un effet causal positif sur l'assiduité des députés. Cela confirmerait notre intuition de départ.
Poursuivons par l'étude de l'effet du parti sur l'assiduité. Cette fois, nous contrôlerons par les variables 'sexe', 'age' et 'region' qui peuvent influencer à la fois l'assiduité et le parti. Nous ne contrôlerons pas par 'nb_mandats' et 'Statut' qui sont certainement des fonctions du parti.
model = sm.ols('assiduite ~ groupe_sigle + sexe + age + region', data = df_reg)
result = model.fit()
print(result.summary())
OLS Regression Results
==============================================================================
Dep. Variable: assiduite R-squared: 0.631
Model: OLS Adj. R-squared: 0.614
Method: Least Squares F-statistic: 36.11
Date: Mon, 07 Dec 2020 Prob (F-statistic): 1.26e-93
Time: 02:56:41 Log-Likelihood: -889.75
No. Observations: 531 AIC: 1829.
Df Residuals: 506 BIC: 1936.
Df Model: 24
Covariance Type: nonrobust
========================================================================================================
coef std err t P>|t| [0.025 0.975]
--------------------------------------------------------------------------------------------------------
Intercept 0.6442 0.433 1.488 0.137 -0.207 1.495
groupe_sigle[T.GDR] 3.9974 0.470 8.504 0.000 3.074 4.921
groupe_sigle[T.LFI] 6.1164 0.447 13.675 0.000 5.238 6.995
groupe_sigle[T.LR] 1.5198 0.345 4.401 0.000 0.841 2.198
groupe_sigle[T.LREM] -0.9949 0.323 -3.079 0.002 -1.630 -0.360
groupe_sigle[T.LT] 1.2387 0.460 2.690 0.007 0.334 2.143
groupe_sigle[T.MODEM] -0.0515 0.368 -0.140 0.889 -0.774 0.671
groupe_sigle[T.NI] 0.2146 0.411 0.522 0.602 -0.593 1.023
groupe_sigle[T.SOC] 2.1714 0.421 5.152 0.000 1.343 2.999
groupe_sigle[T.UDI] 1.1950 0.457 2.617 0.009 0.298 2.092
sexe[T.H] 0.0269 0.122 0.221 0.826 -0.213 0.266
region[T.Bourgogne-Franche-Comté] -0.4117 0.312 -1.318 0.188 -1.025 0.202
region[T.Bretagne] -0.1373 0.329 -0.417 0.677 -0.784 0.509
region[T.Centre-Val de Loire] -0.2096 0.327 -0.640 0.522 -0.853 0.433
region[T.Corse] -1.2760 0.733 -1.741 0.082 -2.716 0.164
region[T.Grand Est] 0.3225 0.265 1.216 0.224 -0.198 0.843
region[T.Hauts-de-France] -0.2725 0.269 -1.012 0.312 -0.801 0.256
region[T.Ile-de-France] -0.1930 0.223 -0.866 0.387 -0.631 0.245
region[T.Normandie] 0.4193 0.317 1.323 0.186 -0.203 1.042
region[T.Nouvelle-Aquitaine] -0.5037 0.264 -1.908 0.057 -1.022 0.015
region[T.Occitanie] -0.3888 0.263 -1.477 0.140 -0.906 0.128
region[T.Outre mer] -1.9796 0.283 -6.983 0.000 -2.537 -1.423
region[T.Pays de la Loire] -0.5830 0.309 -1.887 0.060 -1.190 0.024
region[T.Provence-Alpes-Côte d'Azur] -0.4344 0.280 -1.553 0.121 -0.984 0.115
age -0.0117 0.006 -2.109 0.035 -0.023 -0.001
==============================================================================
Omnibus: 106.586 Durbin-Watson: 1.953
Prob(Omnibus): 0.000 Jarque-Bera (JB): 257.009
Skew: 1.027 Prob(JB): 1.55e-56
Kurtosis: 5.719 Cond. No. 940.
==============================================================================
Notes:
[1] Standard Errors assume that the covariance matrix of the errors is correctly specified.
Toutes choses égales par ailleurs dans cette régression, le fait d'appartenir au groupe LFI, GDR ou SOC augmente en moyenne le score d'assiduité de 6.12, 4.0 ou 2.17 points. Le fait d'appartenir à la majorité LREM ou MODEM diminue en moyenne ce score de 1.0 ou 0.05 points. Globalement, on constate que les partis d'opposition ont des coefficients positifs, tandis que la majorité a ses deux coefficients négatifs. Le fait d'appartenir à la majorité diminuerait donc bien l'assiduité comme nous l'avions prédit.
Pour finaliser l'étayement de nos intuitions, étudions maintenant l'impact du statut des députés sur leur assiduité. Nous contrôlerons par 'groupe_sigle', 'age' et 'region', les autres variables étant a priori sans lien avec le statut.
model = sm.ols('assiduite ~ Statut + groupe_sigle + age + region', data = df_reg)
result = model.fit()
print(result.summary())
OLS Regression Results
==============================================================================
Dep. Variable: assiduite R-squared: 0.634
Model: OLS Adj. R-squared: 0.615
Method: Least Squares F-statistic: 33.56
Date: Mon, 07 Dec 2020 Prob (F-statistic): 8.55e-93
Time: 02:56:41 Log-Likelihood: -887.93
No. Observations: 531 AIC: 1830.
Df Residuals: 504 BIC: 1945.
Df Model: 26
Covariance Type: nonrobust
========================================================================================================
coef std err t P>|t| [0.025 0.975]
--------------------------------------------------------------------------------------------------------
Intercept -0.0002 0.605 -0.000 1.000 -1.190 1.189
Statut[T.arrive en cours de mandat] 0.3802 0.481 0.791 0.429 -0.564 1.325
Statut[T.elu pour la 1ere fois] 0.6543 0.411 1.591 0.112 -0.154 1.462
Statut[T.sortant] 0.6490 0.409 1.587 0.113 -0.154 1.452
groupe_sigle[T.GDR] 4.0269 0.469 8.585 0.000 3.105 4.948
groupe_sigle[T.LFI] 6.1021 0.449 13.590 0.000 5.220 6.984
groupe_sigle[T.LR] 1.5622 0.347 4.499 0.000 0.880 2.244
groupe_sigle[T.LREM] -1.0025 0.325 -3.088 0.002 -1.640 -0.365
groupe_sigle[T.LT] 1.2427 0.464 2.678 0.008 0.331 2.154
groupe_sigle[T.MODEM] -0.0463 0.371 -0.125 0.901 -0.776 0.683
groupe_sigle[T.NI] 0.2079 0.412 0.505 0.614 -0.601 1.017
groupe_sigle[T.SOC] 2.2155 0.426 5.199 0.000 1.378 3.053
groupe_sigle[T.UDI] 1.1882 0.456 2.604 0.009 0.292 2.085
region[T.Bourgogne-Franche-Comté] -0.4581 0.313 -1.465 0.144 -1.073 0.156
region[T.Bretagne] -0.1540 0.328 -0.470 0.639 -0.798 0.490
region[T.Centre-Val de Loire] -0.2030 0.327 -0.621 0.535 -0.845 0.439
region[T.Corse] -1.3215 0.741 -1.784 0.075 -2.777 0.134
region[T.Grand Est] 0.2905 0.265 1.098 0.273 -0.229 0.810
region[T.Hauts-de-France] -0.2997 0.269 -1.116 0.265 -0.827 0.228
region[T.Ile-de-France] -0.2116 0.224 -0.945 0.345 -0.651 0.228
region[T.Normandie] 0.4132 0.316 1.307 0.192 -0.208 1.034
region[T.Nouvelle-Aquitaine] -0.5229 0.263 -1.987 0.047 -1.040 -0.006
region[T.Occitanie] -0.4136 0.263 -1.572 0.117 -0.930 0.103
region[T.Outre mer] -1.9537 0.285 -6.859 0.000 -2.513 -1.394
region[T.Pays de la Loire] -0.5941 0.310 -1.916 0.056 -1.203 0.015
region[T.Provence-Alpes-Côte d'Azur] -0.4597 0.280 -1.643 0.101 -1.009 0.090
age -0.0108 0.006 -1.858 0.064 -0.022 0.001
==============================================================================
Omnibus: 101.347 Durbin-Watson: 1.924
Prob(Omnibus): 0.000 Jarque-Bera (JB): 242.533
Skew: 0.982 Prob(JB): 2.16e-53
Kurtosis: 5.666 Cond. No. 966.
==============================================================================
Notes:
[1] Standard Errors assume that the covariance matrix of the errors is correctly specified.
Nous remarquons cette fois que notre intuition de départ était erronée : les nouveaux élus semblent aussi assidus que les anciens. Cela s'explique par la constatation précédente : le fait d'être de la majorité entraîne une baisse d'assiduité ; or LREM comporte énormément de nouveux élus (car c'est un nouveau parti) ; il y avait donc un biais dans notre interprétation initiale.
Nous n'avons cependant pas encore regardé l'influence du nombre de mandats sur l'assiduité. Cela finalisera notre étude.
'nb_mandats' est susceptible d'être influencé par 'sexe', 'groupe_sigle', 'Statut', 'age' et 'region' sans que ces variables soient des fonctions du nombre de mandats.
model = sm.ols('assiduite ~ nb_mandats + sexe + groupe_sigle + Statut + age + region', data = df_reg)
result = model.fit()
print(result.summary())
OLS Regression Results
==============================================================================
Dep. Variable: assiduite R-squared: 0.634
Model: OLS Adj. R-squared: 0.614
Method: Least Squares F-statistic: 31.06
Date: Mon, 07 Dec 2020 Prob (F-statistic): 2.61e-91
Time: 02:56:41 Log-Likelihood: -887.83
No. Observations: 531 AIC: 1834.
Df Residuals: 502 BIC: 1958.
Df Model: 28
Covariance Type: nonrobust
========================================================================================================
coef std err t P>|t| [0.025 0.975]
--------------------------------------------------------------------------------------------------------
Intercept -0.0869 0.638 -0.136 0.892 -1.340 1.166
sexe[T.H] 0.0344 0.124 0.278 0.781 -0.209 0.278
groupe_sigle[T.GDR] 4.0232 0.471 8.547 0.000 3.098 4.948
groupe_sigle[T.LFI] 6.1175 0.453 13.518 0.000 5.228 7.007
groupe_sigle[T.LR] 1.5581 0.348 4.476 0.000 0.874 2.242
groupe_sigle[T.LREM] -0.9895 0.327 -3.025 0.003 -1.632 -0.347
groupe_sigle[T.LT] 1.2541 0.466 2.693 0.007 0.339 2.169
groupe_sigle[T.MODEM] -0.0413 0.373 -0.111 0.912 -0.773 0.691
groupe_sigle[T.NI] 0.2157 0.413 0.522 0.602 -0.596 1.027
groupe_sigle[T.SOC] 2.2394 0.431 5.200 0.000 1.393 3.085
groupe_sigle[T.UDI] 1.1867 0.457 2.595 0.010 0.288 2.085
Statut[T.arrive en cours de mandat] 0.4013 0.485 0.827 0.408 -0.552 1.354
Statut[T.elu pour la 1ere fois] 0.6799 0.416 1.634 0.103 -0.138 1.498
Statut[T.sortant] 0.6668 0.412 1.620 0.106 -0.142 1.476
region[T.Bourgogne-Franche-Comté] -0.4666 0.314 -1.486 0.138 -1.083 0.150
region[T.Bretagne] -0.1665 0.330 -0.504 0.614 -0.815 0.482
region[T.Centre-Val de Loire] -0.2116 0.328 -0.645 0.519 -0.856 0.433
region[T.Corse] -1.3410 0.746 -1.798 0.073 -2.807 0.125
region[T.Grand Est] 0.2805 0.267 1.052 0.293 -0.243 0.804
region[T.Hauts-de-France] -0.3118 0.271 -1.152 0.250 -0.844 0.220
region[T.Ile-de-France] -0.2192 0.225 -0.974 0.330 -0.661 0.223
region[T.Normandie] 0.4066 0.317 1.282 0.200 -0.217 1.030
region[T.Nouvelle-Aquitaine] -0.5359 0.266 -2.017 0.044 -1.058 -0.014
region[T.Occitanie] -0.4206 0.264 -1.593 0.112 -0.939 0.098
region[T.Outre mer] -1.9513 0.287 -6.809 0.000 -2.514 -1.388
region[T.Pays de la Loire] -0.5927 0.311 -1.905 0.057 -1.204 0.019
region[T.Provence-Alpes-Côte d'Azur] -0.4605 0.280 -1.643 0.101 -1.011 0.090
nb_mandats 0.0437 0.131 0.333 0.739 -0.214 0.301
age -0.0111 0.006 -1.877 0.061 -0.023 0.001
==============================================================================
Omnibus: 101.943 Durbin-Watson: 1.924
Prob(Omnibus): 0.000 Jarque-Bera (JB): 245.612
Skew: 0.985 Prob(JB): 4.63e-54
Kurtosis: 5.687 Cond. No. 978.
==============================================================================
Notes:
[1] Standard Errors assume that the covariance matrix of the errors is correctly specified.
Étonnamment, le nombre de mandats ne semble avoir qu'un très faible impact sur l'assiduité des députés (valeur faible et coefficient non significatif).
Notre étude nous a conduits à appréhender l'assiduité des députés sous un angle plus général que celui de la seule présence. Une analyse en composantes principales nous a permis de synthétiser l'activité des députés à l'Assemblée grâce à un nombre réduit de variables.
Par la suite, un clustering nous a permis d'identifier clairement deux catégories de députés ; même si cette segmentation n'est pas parfaite car il y a en réalité un continuum d'assiduité, nous avons bien pu obverver qu'un groupe de députés se distinguait de la masse par son activité.
Une analyse descriptive et comparative des deux clusters nous a permis de dégager les grandes tendances caractérisant les députés assidus.
Enfin, des régressions linéaires nous ont permis d'estimer l'effet causal de variables explicatives sur l'assiduité afin de déterminer plus précisément le profil du député assidu.
Finalement, selon notre étude, le député assidu est un homme de l'opposition (plus particulièrement de l'opposition de gauche, voire d'extrême gauche).
Ce profilage nous apparaît crédible. Nous pourrions maintenant nous demander s'il s'agit là d'un fait d'époque, ou si le député assidu a toujours eu ce profil dans les législatures précédentes, et si oui, dans quelle mesure.